package com.tools.middle.elasticsearch;

import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.mget.MultiGetOperation;
import co.elastic.clients.elasticsearch.indices.*;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tools.common.exception.IORuntimeException;
import com.tools.common.io.ProfileKit;
import com.tools.common.object.json.DeserializerHandler;
import com.tools.common.object.json.JsonKit;
import com.tools.common.object.Note;
import com.tools.common.object.json.SerializerHandler;
import com.tools.common.object.string.StrKit;
import com.tools.common.object.string.VerifyKit;
import com.tools.common.thread.Timeunit;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * 操作 ElasticSearch 的工具类
 * 演示: https://blog.csdn.net/qq_41694906/article/details/134060527
 * */
@Note("操作 ElasticSearch 的工具类。" +
        "演示: https://blog.csdn.net/qq_41694906/article/details/134060527")
public final class ESKit {

    private ESKit() {}

    @Note("ElasticSearch 同步操作客户端实例")
    private static volatile ElasticsearchClient CLIENT;

    @Note("ElasticSearch 异步操作客户端实例")
    private static volatile ElasticsearchAsyncClient ASYNC_CLIENT;

    @Note("ElasticSearch 配置项包装类")
    private static ESSource SOURCE;

    @Note("日志输出实例")
    private static Logger LOGGER;

    /* **********************************************************************************
     *
     *          其他方法
     *
     * *********************************************************************************
     * */

    @Note("新建 Http 主机节点对象")
    public static HttpHost newHostNode(String nodeString) {
        if(VerifyKit.isHostAndPort(nodeString)) {
            String[] array = nodeString.split(":");
            int port = Integer.parseInt(array[1]);
            return new HttpHost(array[0], port);
        }
        throw new IllegalArgumentException("ElasticSearch 节点 【" + nodeString + "】 不是标准的 【ip:端口号】 格式字符串");
    }


    @Note("根据 ESSource 配置项包装类里的用户名和密码，构建一个 CredentialsProvider 验证对象")
    public static CredentialsProvider newCredentialsProvider(ESSource esSource) {
        String username = esSource.getUsername();
        String password = esSource.getPassword();
        if(StrKit.isAnyEmpty(username, password)) {
            return null;
        }
        CredentialsProvider cp = new BasicCredentialsProvider();
        cp.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(username, password));
        return cp;
    }


    @Note("根据 ESSource 配置项包装类里的配置项构建 RestClient 客户端实例")
    public static RestClient newRestClient(ESSource source) {
        return private_newRestClient(source, null);
    }

    @Note("构建默认的 ElasticsearchTransport 的对象，其中包含了对 JSON 字符串的序列化和反序列化映射操作")
    public static ElasticsearchTransport newElasticsearchTransport(RestClient restClient) {
        JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper();
        ObjectMapper objectMapper = jsonpMapper.objectMapper();
        JsonKit.configMapperSkipUnknownField(objectMapper);
        JsonKit.configMapperRWByEnumToString(objectMapper);
        JsonKit.configMapperCharArrayToArray(objectMapper);
        JsonKit.configMapperAccessAllField(objectMapper);
        JsonKit.configMapperSingleValueAsArray(objectMapper);
        JsonKit.addCustomDeserializers(objectMapper, DeserializerHandler.all());
        JsonKit.addCustomSerializers(objectMapper, SerializerHandler.all());
        return new RestClientTransport(restClient, jsonpMapper);
    }

    @Note("关闭指定的 ElasticSearch 客户端实例")
    public static void close(RestClient client) {
        if(client != null) {
            try {
                client.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
        }
    }

    /* **********************************************************************************
     *
     *          索引操作
     *
     * *********************************************************************************
     * */

    @Note("使用本工具类默认的异步客户端，异步创建索引")
    public static CompletableFuture<CreateIndexResponse> createIndicesAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return createIndicesAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，异步创建索引")
    public static CompletableFuture<CreateIndexResponse> createIndicesAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().create(
                cb -> {
                    cb.index(builder.getFirstIndicesName())
                            .settings(
                                    ib -> ib.numberOfShards(String.valueOf(builder.getNumberOfShards()))
                                            .numberOfReplicas(String.valueOf(builder.getNumberOfReplicas()))
                                            .analysis(az -> az.analyzer(builder.getAnalyzer(),
                                                    z->z.custom(a -> a.tokenizer(builder.getTokenizer()))))
                            )
                            .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
                            .timeout(Time.of(tb -> tb.time(builder.getTimeout())));
                    if(builder.useMappings()) cb.mappings(map -> map.properties(builder.getMappings()));
                    return cb;
                }
        );
    }

    private static void private_fillInMapping(CreateIndexRequest.Builder crb, ESIndicesBuilder builder) {

    }

    @Note("使用本工具类默认的同步客户端，同步创建索引")
    public static CreateIndexResponse createIndices(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return createIndices(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，同步创建索引")
    public static CreateIndexResponse createIndices(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
            return client.indices().create(
                    cb -> {
                        if(builder.useMappings()) cb.mappings(map -> map.properties(builder.getMappings()));
                        cb.index(builder.getFirstIndicesName())
                                .settings(
                                        ib -> ib.numberOfShards(String.valueOf(builder.getNumberOfShards()))
                                                .numberOfReplicas(String.valueOf(builder.getNumberOfReplicas()))
                                                .analysis(az -> az.analyzer(builder.getAnalyzer(),
                                                        z->z.custom(a -> a.tokenizer(builder.getTokenizer()))))
                                )
                                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())));
                        return cb;
                    }
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，异步删除索引")
    public static CompletableFuture<DeleteIndexResponse> deleteIndicesAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return deleteIndicesAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，异步删除索引")
    public static CompletableFuture<DeleteIndexResponse> deleteIndicesAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().delete(dlb ->
                dlb.index(builder.getIndicesNames())
                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，同步删除索引")
    public static DeleteIndexResponse deleteIndices(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return deleteIndices(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，同步删除索引")
    public static DeleteIndexResponse deleteIndices(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
            return client.indices().delete(dlb -> dlb
                    .index(builder.getIndicesNames())
                    .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                    .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，异步获取索引信息")
    public static CompletableFuture<GetIndexResponse> getIndicesAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return getIndicesAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，异步获取索引信息")
    public static CompletableFuture<GetIndexResponse> getIndicesAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().get(grb -> grb
                .index(builder.getIndicesNames())
                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，同步获取索引的信息")
    public static GetIndexResponse getIndices(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return getIndices(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，同步获取索引的信息")
    public static GetIndexResponse getIndices(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
            return client.indices().get(grb -> grb
                    .index(builder.getIndicesNames())
                    .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，异步打开索引")
    public static CompletableFuture<OpenResponse> openIndicesAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return openIndicesAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，异步打开索引")
    public static CompletableFuture<OpenResponse> openIndicesAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().open(grb -> grb
                .index(builder.getIndicesNames())
                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，同步打开索引")
    public static OpenResponse openIndices(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return openIndices(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，同步打开索引")
    public static OpenResponse openIndices(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
            return client.indices().open(grb -> grb
                    .index(builder.getIndicesNames())
                    .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                    .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的异步客户端，异步关闭索引")
    public static CompletableFuture<CloseIndexResponse> closeIndicesAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return closeIndicesAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，异步关闭索引")
    public static CompletableFuture<CloseIndexResponse> closeIndicesAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().close(crb -> crb
                .index(builder.getIndicesNames())
                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，同步关闭索引")
    public static CloseIndexResponse closeIndices(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return closeIndices(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，同步关闭索引")
    public static CloseIndexResponse closeIndices(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
            return client.indices().close(crb -> crb
                    .index(builder.getIndicesNames())
                    .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                    .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，修改分片")
    public static CompletableFuture<PutIndicesSettingsResponse> modifySettingAsync(ESIndicesBuilder builder) {
        private_lazyLoadAsyncClient();
        return modifySettingAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，修改分片")
    public static CompletableFuture<PutIndicesSettingsResponse> modifySettingAsync(ElasticsearchAsyncClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        return client.indices().putSettings(ps -> ps
                .index(builder.getIndicesNames())
                .settings(
                        is -> is.numberOfShards(String.valueOf(builder.getNumberOfShards()))
                                .analysis(az -> az.analyzer(builder.getAnalyzer(),
                                        z -> z.custom(a -> a.tokenizer(builder.getTokenizer()))))
                                .numberOfReplicas(String.valueOf(builder.getNumberOfReplicas()))
                )
                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，修改分片")
    public static PutIndicesSettingsResponse modifySetting(ESIndicesBuilder builder) {
        private_lazyLoadSyncClient();
        return modifySetting(CLIENT, builder);
    }


    @Note("使用指定的同步客户端，修改分片")
    public static PutIndicesSettingsResponse modifySetting(ElasticsearchClient client, ESIndicesBuilder builder) {
        private_indicesBuilderNotNull(builder);
        try {
           return client.indices().putSettings(ps -> ps
                   .index(builder.getIndicesNames())
                   .settings(
                           is -> is.numberOfShards(String.valueOf(builder.getNumberOfShards()))
                                   .analysis(az -> az.analyzer(builder.getAnalyzer(),
                                           z -> z.custom(a -> a.tokenizer(builder.getTokenizer()))))
                                   .numberOfReplicas(String.valueOf(builder.getNumberOfReplicas())))
                   .masterTimeout(Time.of(mtb -> mtb.time(builder.getMasterNodeTimeout())))
                   .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
           );
       } catch (IOException e) {
           throw new IORuntimeException(e);
       }
    }

    /* **********************************************************************************
     *
     *          文档操作
     *
     * *********************************************************************************
     * */

    @Note("使用本工具类默认的异步客户端，新增文档或者替换已有的旧文档")
    public static CompletableFuture<CreateResponse>  putDocumentAsync(ESDocBuilder builder) {
        private_lazyLoadAsyncClient();
        return putDocumentAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，新增文档或者替换已有的旧文档")
    public static CompletableFuture<CreateResponse>  putDocumentAsync(ElasticsearchAsyncClient client, ESDocBuilder builder) {
        private_docBuilderNotNull(builder);
        return client.create(
                ctb -> ctb.index(builder.getIndicesName())
                        .id(builder.getId())
                        .document(builder.getDocument())
                        .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，新增文档或者替换已有的旧文档")
    public static CreateResponse putDocument(ESDocBuilder builder) {
        private_lazyLoadSyncClient();
        return putDocument(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，新增文档或者替换已有的旧文档")
    public static CreateResponse putDocument(ElasticsearchClient client, ESDocBuilder builder) {
        private_docBuilderNotNull(builder);
        try {
            return client.create(
                    ctb -> ctb.index(builder.getIndicesName())
                            .id(builder.getId())
                            .document(builder.getDocument())
                            .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，修改文档的数据")
    public static <T> CompletableFuture<UpdateResponse<T>> updateDocumentAsync(ESDocBuilder builder, Class<T> documentType) {
        private_lazyLoadAsyncClient();
        return updateDocumentAsync(ASYNC_CLIENT, builder, documentType);
    }

    @Note("使用指定的异步客户端，修改文档的数据")
    public static <T> CompletableFuture<UpdateResponse<T>> updateDocumentAsync(ElasticsearchAsyncClient client, ESDocBuilder builder, Class<T> documentType) {
        private_docBuilderNotNull(builder);
        private_docTypeNotNull(documentType);
        return client.update(ub -> ub.index(builder.getIndicesName())
                        .id(builder.getId())
                        .doc(builder.getDocument())
                        .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                , documentType);
    }


    @Note("使用本工具类默认的同步客户端，修改文档的数据")
    public static <T> UpdateResponse<T> updateDocument(ESDocBuilder builder, Class<T> documentType) {
        private_lazyLoadSyncClient();
        return updateDocument(CLIENT, builder, documentType);
    }

    @Note("使用指定的同步客户端，修改文档的数据")
    public static <T> UpdateResponse<T> updateDocument(ElasticsearchClient client, ESDocBuilder builder, Class<T> documentType) {
        private_docBuilderNotNull(builder);
        private_docTypeNotNull(documentType);
        try {
            return client.update(ub -> ub.index(builder.getIndicesName())
                            .id(builder.getId())
                            .doc(builder.getDocument())
                            .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，删除文档的数据")
    public static CompletableFuture<DeleteResponse> deleteDocumentAsync(ESDocBuilder builder) {
        private_lazyLoadAsyncClient();
        return deleteDocumentAsync(ASYNC_CLIENT, builder);
    }

    @Note("使用指定的异步客户端，删除文档的数据")
    public static CompletableFuture<DeleteResponse> deleteDocumentAsync(ElasticsearchAsyncClient client, ESDocBuilder builder) {
        private_docBuilderNotNull(builder);
        return client.delete(ddb -> ddb.index(builder.getIndicesName())
                .id(builder.getId())
                .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
        );
    }

    @Note("使用本工具类默认的同步客户端，删除文档的数据")
    public static DeleteResponse deleteDocument(ESDocBuilder builder) {
        private_lazyLoadSyncClient();
        return deleteDocument(CLIENT, builder);
    }

    @Note("使用指定的同步客户端，删除文档的数据")
    public static DeleteResponse deleteDocument(ElasticsearchClient client, ESDocBuilder builder) {
        private_docBuilderNotNull(builder);
        try {
            return client.delete(ddb -> ddb.index(builder.getIndicesName())
                            .id(builder.getId())
                            .timeout(Time.of(tb -> tb.time(builder.getTimeout())))
            );
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的同步客户端，获取文档的数据")
    public static <T> CompletableFuture<GetResponse<T>> getDocumentAsync(ESDocBuilder builder, Class<T> documentType) {
        private_lazyLoadAsyncClient();
        return getDocumentAsync(ASYNC_CLIENT, builder, documentType);
    }

    @Note("使用指定的同步客户端，获取文档的数据")
    public static <T> CompletableFuture<GetResponse<T>> getDocumentAsync(ElasticsearchAsyncClient client, ESDocBuilder builder, Class<T> documentType) {
        private_docBuilderNotNull(builder);
        private_docTypeNotNull(documentType);
        return client.get(gdb -> gdb.index(builder.getIndicesName()).id(builder.getId()), documentType);
    }


    @Note("使用本工具类默认的同步客户端，获取文档的数据")
    public static <T>GetResponse<T> getDocument(ESDocBuilder builder, Class<T> documentType) {
        private_lazyLoadSyncClient();
        return getDocument(CLIENT, builder, documentType);
    }

    @Note("使用指定的同步客户端，获取文档的数据")
    public static <T> GetResponse<T> getDocument(ElasticsearchClient client, ESDocBuilder builder, Class<T> documentType) {
        private_docBuilderNotNull(builder);
        private_docTypeNotNull(documentType);
        try {
            return client.get(gdb -> gdb.index(builder.getIndicesName()).id(builder.getId()), documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，批量增删改文档")
    public static CompletableFuture<BulkResponse> bulkDocumentAsync(ESDocBuilder... docBuilders) {
        private_lazyLoadAsyncClient();
        return bulkDocumentAsync(ASYNC_CLIENT, 1, Timeunit.SECOND, docBuilders);
    }

    @Note("使用本工具类默认的异步客户端，批量增删改文档，同时指定超时时间")
    public static CompletableFuture<BulkResponse> bulkDocumentAsync(long timeout, Timeunit timeunit, ESDocBuilder... docBuilders) {
        private_lazyLoadAsyncClient();
        return bulkDocumentAsync(ASYNC_CLIENT, timeout, timeunit, docBuilders);
    }

    @Note("使用指定的异步客户端，批量增删改文档")
    public static CompletableFuture<BulkResponse> bulkDocumentAsync(ElasticsearchAsyncClient client, ESDocBuilder... docBuilders) {
        return bulkDocumentAsync(client, 1, Timeunit.SECOND, docBuilders);
    }

    @Note("使用指定的异步客户端，批量增删改文档，同时指定超时时间")
    public static CompletableFuture<BulkResponse> bulkDocumentAsync(ElasticsearchAsyncClient client, long timeout, Timeunit timeunit, ESDocBuilder... docBuilders) {
        private_asyncClientNotNull(client);
        String timeoutString = timeunit.getHasUnitTime(timeout);
        List<BulkOperation> bulkOperations = private_checkDocBuildersAndGetBulkList(docBuilders);
        return client.bulk(bb -> bb.operations(bulkOperations).timeout(Time.of(tb -> tb.time(timeoutString))));
    }

    @Note("使用本工具类默认的同步客户端，批量增删改文档")
    public static BulkResponse bulkDocument(ESDocBuilder... docBuilders) {
        private_lazyLoadSyncClient();
        return bulkDocument(CLIENT, 1, Timeunit.SECOND, docBuilders);
    }

    @Note("使用本工具类默认的同步客户端，批量增删改文档，同时指定超时时间")
    public static BulkResponse bulkDocument(long timeout, Timeunit timeunit, ESDocBuilder... docBuilders) {
        private_lazyLoadSyncClient();
        return bulkDocument(CLIENT, timeout, timeunit, docBuilders);
    }

    @Note("使用指定的同步客户端，批量增删改文档")
    public static BulkResponse bulkDocument(ElasticsearchClient client, ESDocBuilder... docBuilders) {
        return bulkDocument(client, 1, Timeunit.SECOND, docBuilders);
    }

    @Note("使用指定的同步客户端，批量增删改文档，同时指定超时时间")
    public static BulkResponse bulkDocument(ElasticsearchClient client, long timeout, Timeunit timeunit, ESDocBuilder... docBuilders) {
        private_syncClientNotNull(client);
        String timeoutString = timeunit.getHasUnitTime(timeout);
        List<BulkOperation> bulkOperations = private_checkDocBuildersAndGetBulkList(docBuilders);
        try {
            return client.bulk(bb -> bb.operations(bulkOperations).timeout(Time.of(tb -> tb.time(timeoutString))));
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的异步客户端，批量获取文档")
    public static <T> CompletableFuture<MgetResponse<T>> getBatchDocumentAsync(Class<T> documentType, ESDocBuilder... docBuilders) {
        private_lazyLoadAsyncClient();
        return getBatchDocumentAsync(ASYNC_CLIENT, documentType, docBuilders);
    }

    @Note("使用指定的异步客户端，批量获取文档")
    public static <T> CompletableFuture<MgetResponse<T>> getBatchDocumentAsync(ElasticsearchAsyncClient client, Class<T> documentType, ESDocBuilder... docBuilders) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        List<MultiGetOperation> operations = private_checkDocBuildersAndGetMultiGetOperations(docBuilders);
        return client.mget(mrb -> mrb.docs(operations), documentType);
    }

    @Note("使用本工具类默认的同步客户端，批量获取文档")
    public static <T> MgetResponse<T> getBatchDocument(Class<T> documentType, ESDocBuilder... docBuilders) {
        private_lazyLoadSyncClient();
        return getBatchDocument(CLIENT, documentType, docBuilders);
    }

    @Note("使用指定的同步客户端，批量获取文档")
    public static <T> MgetResponse<T> getBatchDocument(ElasticsearchClient client, Class<T> documentType, ESDocBuilder... docBuilders) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        List<MultiGetOperation> operations = private_checkDocBuildersAndGetMultiGetOperations(docBuilders);
        try {
            return client.mget(mrb -> mrb.docs(operations), documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    /* **********************************************************************************
     *
     *          全文搜索方法
     *
     * *********************************************************************************
     * */

    @Note("使用本工具类默认的同步客户端，进行普通的 ?q=xxx 查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchQAsync(Class<T> documentType, QSearch qSearch) {
        private_lazyLoadAsyncClient();
        return searchQAsync(ASYNC_CLIENT, documentType, qSearch);
    }

    @Note("使用指定的异步客户端，进行普通的 ?q=xxx 查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchQAsync(ElasticsearchAsyncClient client, Class<T> documentType, QSearch qSearch) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(qSearch);
        return client.search(b -> private_searchReqBuilderSetData(b.q(qSearch.getQ()), qSearch), documentType);
    }

    @Note("使用本工具类默认的同步客户端，进行普通的 ?q=xxx 查询")
    public static <T> SearchResponse<T> searchQ(Class<T> documentType, QSearch qSearch) {
        private_lazyLoadSyncClient();
        return searchQ(CLIENT, documentType, qSearch);
    }

    @Note("使用指定的异步客户端，进行多字段的 ?q=xxx 查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchStringAsync(Class<T> documentType, StringSearch qSearch) {
        private_lazyLoadAsyncClient();
        return searchStringAsync(ASYNC_CLIENT, documentType, qSearch);
    }

    @Note("使用指定的异步客户端，进行普通的 ?q=xxx 查询")
    public static <T> SearchResponse<T> searchQ(ElasticsearchClient client, Class<T> documentType, QSearch qSearch) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(qSearch);
        try {
            return client.search(b -> private_searchReqBuilderSetData(b.q(qSearch.getQ()), qSearch), documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用指定的同步客户端，进行多字段的 ?q=xxx 查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchStringAsync(ElasticsearchAsyncClient client, Class<T> documentType, StringSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndFieldsNotEmpty(search);
        return client.search(b -> {
                    b.query(q -> q.queryString(qs -> {
                                        qs.fields(search.getFields()).query(search.getQuery())
                                                .fuzziness(String.valueOf(search.getFuzziness()));
                                        return search.useAnalyzer() ? qs.analyzer(search.getAnalyzer()) : qs;
                                    })
                            );
                    return private_searchReqBuilderSetData(b, search);
                }
                , documentType);
    }

    @Note("使用指定的同步客户端，进行多字段的 ?q=xxx 查询")
    public static <T> SearchResponse<T> searchString(Class<T> documentType, StringSearch search) {
        private_lazyLoadSyncClient();
        return searchString(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，进行多字段的 ?q=xxx 查询")
    public static <T> SearchResponse<T> searchString(ElasticsearchClient client, Class<T> documentType, StringSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndFieldsNotEmpty(search);
        try {
            return client.search(b -> private_searchReqBuilderSetData(b, search)
                            .query(q -> q.queryString(qs -> {
                                qs.fields(search.getFields()).query(search.getQuery())
                                        .fuzziness(String.valueOf(search.getFuzziness()));
                        return search.useAnalyzer() ? qs.analyzer(search.getAnalyzer()) : qs;
                    })), documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的异步客户端，来根据多个 ID 查询文档")
    public static <T> CompletableFuture<SearchResponse<T>> searchByIdsAsync(Class<T> documentType, IdsSearch idsSearch) {
        private_lazyLoadAsyncClient();
        return searchByIdsAsync(ASYNC_CLIENT, documentType, idsSearch);
    }

    @Note("使用指定的异步客户端，来根据多个 ID 查询文档")
    public static <T> CompletableFuture<SearchResponse<T>> searchByIdsAsync(ElasticsearchAsyncClient client, Class<T> documentType, IdsSearch idsSearch) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndIdsNotEmpty(idsSearch);
        return client.search(b ->
                        private_searchReqBuilderSetData(b.query(q -> q.ids(sid -> sid.values(idsSearch.getIds()))), idsSearch)
                , documentType);
    }

    @Note("使用本工具类默认的同步客户端，来根据多个 ID 查询文档")
    public static <T> SearchResponse<T> searchByIds(Class<T> documentType, IdsSearch idsSearch) {
        private_lazyLoadSyncClient();
        return searchByIds(CLIENT, documentType, idsSearch);
    }

    @Note("使用指定的同步客户端，来根据多个 ID 查询文档")
    public static <T> SearchResponse<T> searchByIds(ElasticsearchClient client, Class<T> documentType, IdsSearch idsSearch) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndIdsNotEmpty(idsSearch);
        try {
            return client.search(b ->
                            private_searchReqBuilderSetData(b.query(q -> q.ids(sid -> sid.values(idsSearch.getIds()))), idsSearch)
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行范围搜索查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchRangeAsync(Class<T> documentType, RangeSearch search) {
        private_lazyLoadAsyncClient();
        return searchRangeAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行范围搜索查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchRangeAsync(ElasticsearchAsyncClient client, Class<T> documentType, RangeSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> private_searchReqBuilderSetData(b.query(q -> q.range(search::putRange)), search), documentType);
    }

    @Note("使用本工具类默认的同步客户端，来进行范围搜索查询")
    public static <T> SearchResponse<T> searchRange(Class<T> documentType, RangeSearch search) {
        private_lazyLoadSyncClient();
        return searchRange(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来进行范围搜索查询")
    public static <T> SearchResponse<T> searchRange(ElasticsearchClient client, Class<T> documentType, RangeSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> private_searchReqBuilderSetData(b.query(q -> q.range(search::putRange)), search), documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行多字段的匹配查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchInMultiFieldAsync(Class<T> documentType, MultiSearch search) {
        private_lazyLoadAsyncClient();
        return searchInMultiFieldAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行多字段的匹配查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchInMultiFieldAsync(ElasticsearchAsyncClient client, Class<T> documentType, MultiSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndFieldsNotEmpty(search);
        return client.search(b -> {
            b.query(q -> q.multiMatch(m -> {
                m.fields(search.getFields()).fuzziness(String.valueOf(search.getFuzziness()))
                        .query(search.getQuery());
                    return search.useAnalyzer() ? m.analyzer(search.getAnalyzer()) : m;
                    }));
            return private_searchReqBuilderSetData(b, search);
        }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行多字段的匹配查询")
    public static <T> SearchResponse<T> searchInMultiField(Class<T> documentType, MultiSearch search) {
        private_lazyLoadSyncClient();
        return searchInMultiField(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来进行多字段的匹配查询")
    public static <T> SearchResponse<T> searchInMultiField(ElasticsearchClient client, Class<T> documentType, MultiSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndFieldsNotEmpty(search);
        try {
            return client.search(b -> {
                b.query(q -> q.multiMatch(m -> {
                        m.fields(search.getFields()).query(search.getQuery()).fuzziness(String.valueOf(search.getFuzziness()));
                    return search.useAnalyzer() ? m.analyzer(search.getAnalyzer()) : m;
                }));
                return private_searchReqBuilderSetData(b, search);
                }, documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行单字段分词匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchMatchAsync(Class<T> documentType, MatchSearch search) {
        private_lazyLoadAsyncClient();
        return searchMatchAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行单字段分词匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchMatchAsync(ElasticsearchAsyncClient client, Class<T> documentType, MatchSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
            b.query(q -> q.match(m -> {
                m.field(search.getField())
                        .query(search.getQuery())
                        .fuzziness(search.getFuzzinessString());
                return search.useAnalyzer() ? m.analyzer(search.getAnalyzer()) : m;
            }));
            return private_searchReqBuilderSetData(b, search);
        }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行单字段分词匹配搜索")
    public static <T> SearchResponse<T> searchMatch(Class<T> documentType, MatchSearch search) {
        private_lazyLoadSyncClient();
        return searchMatch(CLIENT, documentType, search);
    }


    @Note("使用指定的同步客户端，来进行单字段分词匹配搜索")
    public static <T> SearchResponse<T> searchMatch(ElasticsearchClient client, Class<T> documentType, MatchSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                b.query(q -> q.match(m -> {
                    m.field(search.getField())
                            .fuzziness(search.getFuzzinessString())
                            .query(search.getQuery());
                    return search.useAnalyzer() ? m.analyzer(search.getAnalyzer()) : m;
                }));
                return private_searchReqBuilderSetData(b, search);
            }, documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的异步客户端，来进行单字段精确匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchTermAsync(Class<T> documentType, TermSearch search) {
        private_lazyLoadAsyncClient();
        return searchTermAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行单字段精确匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchTermAsync(ElasticsearchAsyncClient client, Class<T> documentType, TermSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
            b.query(q -> q.term(t -> t.field(search.getField())
                    .value(search.getQuery())));
            return private_searchReqBuilderSetData(b, search);
        }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行单字段精确匹配搜索")
    public static <T> SearchResponse<T> searchTerm(Class<T> documentType, TermSearch search) {
        private_lazyLoadSyncClient();
        return searchTerm(CLIENT, documentType, search);
    }


    @Note("使用指定的同步客户端，来进行单字段精确匹配搜索")
    public static <T> SearchResponse<T> searchTerm(ElasticsearchClient client, Class<T> documentType, TermSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                b.query(q -> q.term(t -> t.field(search.getField())
                        .value(search.getQuery())));
                return private_searchReqBuilderSetData(b, search);
            }, documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }



    @Note("使用本工具类默认的异步客户端，来进行单字段、多内容的精确匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchTermsAsync(Class<T> documentType, TermsSearch search) {
        private_lazyLoadAsyncClient();
        return searchTermsAsync(ASYNC_CLIENT, documentType, search);
    }


    @Note("使用指定的异步客户端，来进行单字段、多内容的精确匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchTermsAsync(ElasticsearchAsyncClient client, Class<T> documentType, TermsSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndValuesNotEmpty(search);
        return client.search(b -> {
            b.query(q -> q.terms(t -> t.field(search.getField())
                    .terms(ts -> ts.value(search.getValues()))));
            return private_searchReqBuilderSetData(b, search);
        }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行单字段、多内容的精确匹配搜索")
    public static <T> SearchResponse<T> searchTerms(Class<T> documentType, TermsSearch search) {
        private_lazyLoadSyncClient();
        return searchTerms(CLIENT, documentType, search);
    }


    @Note("使用指定的同步客户端，来进行单字段、多内容的精确匹配搜索")
    public static <T> SearchResponse<T> searchTerms(ElasticsearchClient client, Class<T> documentType, TermsSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_indicesNameAndValuesNotEmpty(search);
        try {
            return client.search(b -> {
                b.query(q -> q.terms(t -> t.field(search.getField())
                        .terms(ts -> ts.value(search.getValues()))));
                return private_searchReqBuilderSetData(b, search);
            }, documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行单字段正则匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchRegexpAsync(Class<T> documentType, RegexpSearch search) {
        private_lazyLoadAsyncClient();
        return searchRegexpAsync(ASYNC_CLIENT, documentType, search);
    }


    @Note("使用指定的异步客户端，来进行单字段正则匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchRegexpAsync(ElasticsearchAsyncClient client, Class<T> documentType, RegexpSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
            b.query(q -> q.regexp(r -> r.field(search.getField())
                    .value(search.getPattern())));
            return private_searchReqBuilderSetData(b, search);
        }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行单字段正则匹配搜索")
    public static <T> SearchResponse<T> searchRegexp(Class<T> documentType, RegexpSearch search) {
        private_lazyLoadSyncClient();
        return searchRegexp(CLIENT, documentType, search);
    }


    @Note("使用指定的同步客户端，来进行单字段正则匹配搜索")
    public static <T> SearchResponse<T> searchRegexp(ElasticsearchClient client, Class<T> documentType, RegexpSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                b.query(q -> q.regexp(r -> r.field(search.getField())
                        .value(search.getPattern())));
                return private_searchReqBuilderSetData(b, search);
            }, documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行单字段短语匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchPhraseAsync(Class<T> documentType, PhraseSearch search) {
        private_lazyLoadAsyncClient();
        return searchPhraseAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行单字段短语匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchPhraseAsync(ElasticsearchAsyncClient client, Class<T> documentType, PhraseSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
                    b.query(q -> q.matchPhrase(p ->
                            p.field(search.getField()).query(search.getQuery()).analyzer(search.getAnalyzer())));
                    return private_searchReqBuilderSetData(b, search);
                }
                , documentType);
    }

    @Note("使用本工具类默认的同步客户端，来进行单字段短语匹配搜索")
    public static <T> SearchResponse<T> searchPhrase(Class<T> documentType, PhraseSearch search) {
        private_lazyLoadSyncClient();
        return searchPhrase(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来进行单字段短语匹配搜索")
    public static <T> SearchResponse<T> searchPhrase(ElasticsearchClient client, Class<T> documentType, PhraseSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                        b.query(q -> q.matchPhrase(p ->
                                p.field(search.getField()).query(search.getQuery()).analyzer(search.getAnalyzer())));
                        return private_searchReqBuilderSetData(b, search);
                    }
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行单字段前缀匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchPrefixAsync(Class<T> documentType, PrefixSearch search) {
        private_lazyLoadAsyncClient();
        return searchPrefixAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来进行单字段前缀匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchPrefixAsync(ElasticsearchAsyncClient client, Class<T> documentType, PrefixSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
                    b.query(q -> q.prefix(p -> p.field(search.getField()).value(search.getQuery())));
                    return private_searchReqBuilderSetData(b, search);
                }
                , documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行单字段前缀匹配搜索")
    public static <T> SearchResponse<T> searchPrefix(Class<T> documentType, PrefixSearch search) {
        private_lazyLoadSyncClient();
        return searchPrefix(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来进行单字段前缀匹配搜索")
    public static <T> SearchResponse<T> searchPrefix(ElasticsearchClient client, Class<T> documentType, PrefixSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                        b.query(q -> q.prefix(p -> p.field(search.getField()).value(search.getQuery())));
                        return private_searchReqBuilderSetData(b, search);
                    }
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来模糊匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchFuzzyAsync(Class<T> documentType, FuzzySearch search) {
        private_lazyLoadAsyncClient();
        return searchFuzzyAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的异步客户端，来模糊匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchFuzzyAsync(ElasticsearchAsyncClient client, Class<T> documentType, FuzzySearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
                    b.query(q -> q.fuzzy(f -> f.fuzziness(String.valueOf(search.getFuzziness()))
                            .field(search.getField()).value(search.getQuery())));
                    return private_searchReqBuilderSetData(b, search);
                }
                , documentType);
    }

    @Note("使用本工具类默认的同步客户端，来模糊匹配搜索")
    public static <T> SearchResponse<T> searchFuzzy(Class<T> documentType, FuzzySearch search) {
        private_lazyLoadSyncClient();
        return searchFuzzy(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来模糊匹配搜索")
    public static <T> SearchResponse<T> searchFuzzy(ElasticsearchClient client, Class<T> documentType, FuzzySearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                        b.query(q -> q.fuzzy(f -> f.fuzziness(String.valueOf(search.getFuzziness()))
                                .field(search.getField()).value(search.getQuery())));
                        return private_searchReqBuilderSetData(b, search);
                    }
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("使用本工具类默认的同步客户端，来通配符匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchWildcardAsync(Class<T> documentType, WildcardSearch search) {
        private_lazyLoadAsyncClient();
        return searchWildcardAsync(ASYNC_CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来通配符匹配搜索")
    public static <T> CompletableFuture<SearchResponse<T>> searchWildcardAsync(ElasticsearchAsyncClient client, Class<T> documentType, WildcardSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        return client.search(b -> {
                    b.query(q -> q.wildcard(w -> w.field(search.getField()).value(search.getQuery())));
                    return private_searchReqBuilderSetData(b, search);
                }
                , documentType);
    }


    @Note("使用本工具类默认的同步客户端，来通配符匹配搜索")
    public static <T> SearchResponse<T> searchWildcard(Class<T> documentType, WildcardSearch search) {
        private_lazyLoadSyncClient();
        return searchWildcard(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来通配符匹配搜索")
    public static <T> SearchResponse<T> searchWildcard(ElasticsearchClient client, Class<T> documentType, WildcardSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_searchEntityNotEmptyCheck(search);
        try {
            return client.search(b -> {
                        b.query(q -> q.wildcard(w -> w.field(search.getField()).value(search.getQuery())));
                        return private_searchReqBuilderSetData(b, search);
                    }
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("使用本工具类默认的异步客户端，来进行多条件布尔查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchBoolAsync(Class<T> documentType, BoolSearch search) {
        private_lazyLoadAsyncClient();
        return searchBoolAsync(ASYNC_CLIENT, documentType, search);
    }


    @Note("使用指定的异步客户端，来进行多条件布尔查询")
    public static <T> CompletableFuture<SearchResponse<T>> searchBoolAsync(ElasticsearchAsyncClient client, Class<T> documentType, BoolSearch search) {
        private_asyncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_boolSearchCheck(search);
        return client.search(b -> {
                    b.query(q -> q.bool(qb -> private_boolHandle(qb, search)));
                    return private_searchReqBuilderSetData(b, search);
                }, documentType);
    }


    @Note("使用本工具类默认的同步客户端，来进行多条件布尔查询")
    public static <T> SearchResponse<T> searchBool(Class<T> documentType, BoolSearch search) {
        private_lazyLoadSyncClient();
        return searchBool(CLIENT, documentType, search);
    }

    @Note("使用指定的同步客户端，来进行多条件布尔查询")
    public static <T> SearchResponse<T> searchBool(ElasticsearchClient client, Class<T> documentType, BoolSearch search) {
        private_syncClientNotNull(client);
        private_docTypeNotNull(documentType);
        private_boolSearchCheck(search);
        try {
            return client.search(b -> {
                        b.query(q -> q.bool(qb -> private_boolHandle(qb, search)));
                        return private_searchReqBuilderSetData(b, search);
                    }
                    , documentType);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }


    /* **********************************************************************************
     *
     *          私有逻辑
     *
     * *********************************************************************************
     * */

    @Note("ElasticSearch 同步操作客户端的非空校验")
    private static void private_syncClientNotNull(ElasticsearchClient client) {
        if(client == null) throw new NullPointerException("ElasticSearch 同步操作客户端不能为 null");
    }

    @Note("ElasticSearch 异步操作客户端的非空校验")
    private static void private_asyncClientNotNull(ElasticsearchAsyncClient client) {
        if(client == null) throw new NullPointerException("ElasticSearch 同步操作客户端不能为 null");
    }

    @Note("索引操作相关配置参数包装类实例的非空校验")
    private static void private_indicesBuilderNotNull(ESIndicesBuilder builder) {
        if(builder == null) throw new NullPointerException();
        List<String> indicesNames = builder.getIndicesNames();
        if(indicesNames == null || indicesNames.isEmpty()) throw new IllegalArgumentException("至少需要一个以上的索引名称");
    }

    @Note("文档操作相关配置参数包装类实例的非空校验")
    private static void private_docBuilderNotNull(ESDocBuilder builder) {
        if(builder == null) throw new NullPointerException("文档操作相关配置参数包装类实例为 null");
    }

    @Note("文档的具体字节码类型的非空校验")
    private static void private_docTypeNotNull(Class<?> documentType) {
        if(documentType == null) throw new NullPointerException("文档的字节码类型不能为 null");
    }

    @Note("非空验证 Search 搜索实例和它包装的索引名称列表")
    private static void private_searchEntityNotEmptyCheck(Search search) {
        if(search == null) throw new NullPointerException();
        List<String> indicesNames = search.getIndicesNames();
        if(indicesNames == null || indicesNames.isEmpty()) throw new IllegalArgumentException("至少存在一个以上的索引名称");
    }

    @Note("验证索引名称和多个 ID 是否为空")
    private static void private_indicesNameAndIdsNotEmpty(IdsSearch idsSearch) {
        private_searchEntityNotEmptyCheck(idsSearch);
        List<String> ids = idsSearch.getIds();
        if(ids == null || ids.isEmpty()) throw new IllegalArgumentException("你至少需要传入一个以上的文档 ID");
    }

    @Note("在 StringQuery 中校验索引名称和字段名称是否为空")
    private static void private_indicesNameAndFieldsNotEmpty(MultiFieldInterface search) {
        private_searchEntityNotEmptyCheck((Search) search);
        if(search.checkFieldsIsEmpty()) throw new IllegalArgumentException("至少需要一个以上的字段");
    }

    @Note("在 StringQuery 中校验索引名称和要搜索的内容列表是否为空")
    private static void private_indicesNameAndValuesNotEmpty(TermsSearch search) {
        private_searchEntityNotEmptyCheck(search);
        List<FieldValue> values = search.getValues();
        if(values == null || values.isEmpty()) throw new IllegalArgumentException("至少需要一个以上要精确搜索的内容");
    }


    @Note("多条件布尔查询的构建实例校验")
    private static void private_boolSearchCheck(BoolSearch search) {
        private_searchEntityNotEmptyCheck(search);
        if(!search.useMust() && !search.useShould() && !search.useMustNot()) {
            throw new IllegalArgumentException("多条件布尔查询必须要至少一个以上的条件");
        }
    }

    @Note("批量校验文档构建实例，同时根据每个文档构建实例的操作枚举生成对应的操作实例，最后统一为列表返回")
    private static List<BulkOperation> private_checkDocBuildersAndGetBulkList(ESDocBuilder[] docBuilders) {
        if(docBuilders == null) throw new IllegalArgumentException("批量操作至少需要传入一个以上的文档");
        int length = docBuilders.length;
        if(length == 0) throw new IllegalArgumentException("批量操作至少需要传入一个以上的文档");
        List<BulkOperation> results = new ArrayList<>(length);
        for (ESDocBuilder builder : docBuilders) {
            private_docBuilderNotNull(builder);
            ESDocBulkOpEnum bulkOp = builder.getBulkOpEnum();
            switch (bulkOp) {
                case CREATE: results.add(new BulkOperation.Builder().create(cb -> cb.index(builder.getIndicesName()).id(builder.getId()).document(builder.getDocument())).build()); break;
                case INDEX: results.add(new BulkOperation.Builder().index(ib -> ib.index(builder.getIndicesName()).id(builder.getId()).document(builder.getDocument())).build()); break;
                case DELETE: results.add(new BulkOperation.Builder().delete(db -> db.index(builder.getIndicesName()).id(builder.getId())).build()); break;
                case UPDATE: results.add(new BulkOperation.Builder().update(ub -> ub.index(builder.getIndicesName()).id(builder.getId())).build()); break;
                default: throw new IllegalArgumentException("不知道 " + bulkOp + " 应该执行什么操作");
            }
        }
        return results;
    }

    @Note("非空校验批量的文档构建实例，并把它们生成 MultiGetOperation 列表返回")
    private static List<MultiGetOperation> private_checkDocBuildersAndGetMultiGetOperations(ESDocBuilder[] docBuilders) {
        if(docBuilders == null) throw new NullPointerException("批量获取文档时你至少需要一个以上的 ID 和索引名称");
        int length = docBuilders.length;
        if(length == 0) throw new NullPointerException("批量获取文档时你至少需要一个以上的 ID 和索引名称");
        List<MultiGetOperation> operations = new ArrayList<>(length);
        for (ESDocBuilder builder : docBuilders) {
            private_docBuilderNotNull(builder);
            operations.add(new MultiGetOperation.Builder().index(builder.getIndicesName()).id(builder.getId()).build());
        }
        return operations;
    }

    @Note("懒加载同步操作的 ElasticSearch 客户端实例")
    private static void private_lazyLoadSyncClient() {
        if(CLIENT != null) return;
        synchronized (ESKit.class) {
            if(CLIENT != null) return;
            if(SOURCE == null) SOURCE = ProfileKit.getESSource();
            if(LOGGER == null) LOGGER = LoggerFactory.getLogger(ESKit.class);
            private_nodeLogPrint(LOGGER, SOURCE.getNodes());
            RestClient syncClient = private_newRestClient(SOURCE, LOGGER);
            CLIENT = new ElasticsearchClient(newElasticsearchTransport(syncClient));
            if(ASYNC_CLIENT != null) {
                SOURCE = null;
                LOGGER = null;
            }
        }
    }

    @Note("懒加载异步操作的 ElasticSearch 客户端实例")
    private static void private_lazyLoadAsyncClient() {
        if(ASYNC_CLIENT != null) return;
        synchronized (ESKit.class) {
            if(ASYNC_CLIENT != null) return;
            if(LOGGER == null) LOGGER = LoggerFactory.getLogger(ESKit.class);
            if(SOURCE == null) SOURCE = ProfileKit.getESSource();
            private_nodeLogPrint(LOGGER, SOURCE.getNodes());
            RestClient asyncClient = private_newRestClient(SOURCE, LOGGER);
            ASYNC_CLIENT = new ElasticsearchAsyncClient(newElasticsearchTransport(asyncClient));
            if(CLIENT != null) {
                SOURCE = null;
                LOGGER = null;
            }
        }
    }


    @Note("节点日志打印")
    private static void private_nodeLogPrint(Logger log, HttpHost[] nodes) {
        if(nodes.length == 1) {
            HttpHost node = nodes[0];
            log.info("扫描到单机 ElasticSearch 节点: 【{}】", node.getAddress().getHostAddress() + ":" + node.getPort());
        }
        else {
            StringBuilder builder = new StringBuilder();
            for (HttpHost node : nodes) {
                builder.append('\n')
                        .append(node.getAddress().getHostAddress())
                        .append(":")
                        .append(node.getPort());
            }
            String border = StrKit.LOG_PRINT_BORDER;
            log.info("扫描到多个 ElasticSearch 节点: {}{}\n{}", border, builder.toString(), border);
        }
    }


    @Note("用户名、密码等身份验证的配置逻辑")
    private static RestClient private_newRestClient(ESSource source, Logger log) {
        if(source == null) throw new NullPointerException("ElasticSearch 配置项包装对象为 null");
        HttpHost[] nodes = source.getNodes();
        RestClientBuilder builder = RestClient.builder(nodes);
        CredentialsProvider provider = newCredentialsProvider(source);
        if(provider != null) {
            builder.setHttpClientConfigCallback(hb -> hb.disableAuthCaching()
                    .setDefaultCredentialsProvider(provider));
            if(log != null) {
                log.info("ElasticSearch 身份验证配置已完成!");
            }
        }
        return builder.build();
    }


    @Note("搜索前的配置设置。如设置索引、超时、高亮、排序、分页、分词器、聚合等等操作")
    private static SearchRequest.Builder private_searchReqBuilderSetData(SearchRequest.Builder qb, Search search) {
        qb.index(search.getIndicesNames()).timeout(search.getTimeout());
        if(search.useAnalyzer()) qb.analyzer(search.getAnalyzer());
        if(search.useSort()) qb.sort(search.getSorts());
        if(search.useAgg()) qb.aggregations(search.getAggregations());
        if(search.useHighlight()) qb.highlight(h -> h.fields(search.getHighlights()));
        if(search.useSource()) {
            qb.source(srb -> srb.filter(sf -> {
                if(search.useExcludeFields()) sf.excludes(search.getExcludeFields());
                if(search.useIncludeFields()) sf.includes(search.getIncludeFields());
                return sf;
            }));
        }
        return search.usePaging()
                ? qb.from(search.getFrom()).size(search.getSize()).trackTotalHits(t -> t.enabled(true))
                : qb.trackTotalHits(t -> t.enabled(true));
    }


    @Note("多条件布尔查询，遍历搜索条件的逻辑")
    private static BoolQuery.Builder private_boolHandle(BoolQuery.Builder builder, BoolSearch search) {
        if(search.useMust()) {
            List<Search> mustList = search.getMustList();
            for (Search s : mustList) {
                private_chooseMustSearchMethod(builder, s);
            }
        }
        if(search.useShould()) {
            List<Search> shouldList = search.getShouldList();
            for (Search s : shouldList) {
                private_chooseShouldSearchMethod(builder, s);
            }
        }
        if(search.useMustNot()) {
            List<Search> mustNotList = search.getMustNotList();
            for (Search s : mustNotList) {
                private_chooseMustNotSearchMethod(builder, s);
            }
        }
        return builder;
    }


    @Note("AND 查询选择逻辑")
    private static void private_chooseMustSearchMethod(BoolQuery.Builder builder, Search s) {
        if(s instanceof QSearch || s instanceof BoolSearch) {
            return;
        }
        if(s instanceof StringSearch) {
            StringSearch search = (StringSearch) s;
            builder.must(m -> m.queryString(qs -> {
                qs.fields(search.getFields()).fuzziness(String.valueOf(search.getFuzziness())).query(search.getQuery());
                return search.useAnalyzer() ? qs.analyzer(search.getAnalyzer()) : qs;
            }));
            return;
        }
        if(s instanceof IdsSearch) {
            IdsSearch search = (IdsSearch) s;
            builder.must(m -> m.ids(ib -> ib.values(search.getIds())));
            return;
        }
        if(s instanceof MultiSearch) {
            MultiSearch search = (MultiSearch) s;
            builder.must(m -> m.multiMatch(mb -> {
                mb.fields(search.getFields()).query(search.getQuery())
                        .fuzziness(String.valueOf(search.getFuzziness()));
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof MatchSearch) {
            MatchSearch search = (MatchSearch) s;
            builder.must(m -> m.match(mb -> {
                mb.field(search.getField()).query(search.getQuery())
                        .fuzziness(search.getFuzzinessString());
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof TermSearch) {
            TermSearch search = (TermSearch) s;
            builder.must(m -> m.term(tb -> tb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof RegexpSearch) {
            RegexpSearch search = (RegexpSearch) s;
            builder.must(m -> m.regexp(rb -> rb.field(search.getField()).value(search.getPattern())));
            return;
        }
        if(s instanceof FuzzySearch) {
            FuzzySearch search = (FuzzySearch) s;
            builder.must(m -> m.fuzzy(fb -> fb.field(search.getField()).value(search.getQuery()).fuzziness(search.getFuzzinessString())));
            return;
        }
        if(s instanceof WildcardSearch) {
            WildcardSearch search = (WildcardSearch) s;
            builder.must(m -> m.wildcard(wb -> wb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PrefixSearch) {
            PrefixSearch search = (PrefixSearch) s;
            builder.must(m -> m.prefix(pb -> pb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PhraseSearch) {
            PhraseSearch search = (PhraseSearch) s;
            builder.must(m -> m.matchPhrase(pb -> {
                pb.field(search.getField()).query(search.getQuery());
                return search.useAnalyzer() ? pb.analyzer(search.getAnalyzer()) : pb;
            }));
            return;
        }
        if(s instanceof RangeSearch) {
            RangeSearch search = (RangeSearch) s;
            builder.must(m -> m.range(search::putRange));
            return;
        }
        if(s instanceof TermsSearch) {
            TermsSearch search = (TermsSearch) s;
            builder.must(m -> m.terms(tsb -> tsb.field(search.getField()).terms(t -> t.value(search.getValues()))));
        }
    }


    @Note("OR 查询选择逻辑")
    private static void private_chooseShouldSearchMethod(BoolQuery.Builder builder, Search s) {
        if(s instanceof QSearch || s instanceof BoolSearch) {
            return;
        }
        if(s instanceof StringSearch) {
            StringSearch search = (StringSearch) s;
            builder.should(m -> m.queryString(qs -> {
                qs.fields(search.getFields()).fuzziness(String.valueOf(search.getFuzziness())).query(search.getQuery());
                return search.useAnalyzer() ? qs.analyzer(search.getAnalyzer()) : qs;
            }));
            return;
        }
        if(s instanceof IdsSearch) {
            IdsSearch search = (IdsSearch) s;
            builder.should(m -> m.ids(ib -> ib.values(search.getIds())));
            return;
        }
        if(s instanceof MultiSearch) {
            MultiSearch search = (MultiSearch) s;
            builder.should(m -> m.multiMatch(mb -> {
                mb.fields(search.getFields()).query(search.getQuery())
                        .fuzziness(String.valueOf(search.getFuzziness()));
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof MatchSearch) {
            MatchSearch search = (MatchSearch) s;
            builder.should(m -> m.match(mb -> {
                mb.field(search.getField()).query(search.getQuery())
                        .fuzziness(search.getFuzzinessString());
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof TermSearch) {
            TermSearch search = (TermSearch) s;
            builder.should(m -> m.term(tb -> tb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof RegexpSearch) {
            RegexpSearch search = (RegexpSearch) s;
            builder.should(m -> m.regexp(rb -> rb.field(search.getField()).value(search.getPattern())));
            return;
        }
        if(s instanceof FuzzySearch) {
            FuzzySearch search = (FuzzySearch) s;
            builder.should(m -> m.fuzzy(fb -> fb.field(search.getField()).value(search.getQuery()).fuzziness(search.getFuzzinessString())));
            return;
        }
        if(s instanceof WildcardSearch) {
            WildcardSearch search = (WildcardSearch) s;
            builder.should(m -> m.wildcard(wb -> wb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PrefixSearch) {
            PrefixSearch search = (PrefixSearch) s;
            builder.should(m -> m.prefix(pb -> pb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PhraseSearch) {
            PhraseSearch search = (PhraseSearch) s;
            builder.should(m -> m.matchPhrase(pb -> {
                pb.field(search.getField()).query(search.getQuery());
                return search.useAnalyzer() ? pb.analyzer(search.getAnalyzer()) : pb;
            }));
            return;
        }
        if(s instanceof RangeSearch) {
            RangeSearch search = (RangeSearch) s;
            builder.should(m -> m.range(search::putRange));
            return;
        }
        if(s instanceof TermsSearch) {
            TermsSearch search = (TermsSearch) s;
            builder.should(m -> m.terms(tsb -> tsb.field(search.getField()).terms(t -> t.value(search.getValues()))));
        }
    }




    @Note("NOT 查询选择逻辑")
    private static void private_chooseMustNotSearchMethod(BoolQuery.Builder builder, Search s) {
        if(s instanceof QSearch || s instanceof BoolSearch) {
            return;
        }
        if(s instanceof StringSearch) {
            StringSearch search = (StringSearch) s;
            builder.mustNot(m -> m.queryString(qs -> {
                qs.fields(search.getFields()).fuzziness(String.valueOf(search.getFuzziness())).query(search.getQuery());
                return search.useAnalyzer() ? qs.analyzer(search.getAnalyzer()) : qs;
            }));
            return;
        }
        if(s instanceof IdsSearch) {
            IdsSearch search = (IdsSearch) s;
            builder.mustNot(m -> m.ids(ib -> ib.values(search.getIds())));
            return;
        }
        if(s instanceof MultiSearch) {
            MultiSearch search = (MultiSearch) s;
            builder.mustNot(m -> m.multiMatch(mb -> {
                mb.fields(search.getFields()).query(search.getQuery())
                        .fuzziness(String.valueOf(search.getFuzziness()));
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof MatchSearch) {
            MatchSearch search = (MatchSearch) s;
            builder.mustNot(m -> m.match(mb -> {
                mb.field(search.getField()).query(search.getQuery())
                        .fuzziness(search.getFuzzinessString());
                return search.useAnalyzer() ? mb.analyzer(search.getAnalyzer()) : mb;
            }));
            return;
        }
        if(s instanceof TermSearch) {
            TermSearch search = (TermSearch) s;
            builder.mustNot(m -> m.term(tb -> tb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof RegexpSearch) {
            RegexpSearch search = (RegexpSearch) s;
            builder.mustNot(m -> m.regexp(rb -> rb.field(search.getField()).value(search.getPattern())));
            return;
        }
        if(s instanceof FuzzySearch) {
            FuzzySearch search = (FuzzySearch) s;
            builder.mustNot(m -> m.fuzzy(fb -> fb.field(search.getField()).value(search.getQuery()).fuzziness(search.getFuzzinessString())));
            return;
        }
        if(s instanceof WildcardSearch) {
            WildcardSearch search = (WildcardSearch) s;
            builder.mustNot(m -> m.wildcard(wb -> wb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PrefixSearch) {
            PrefixSearch search = (PrefixSearch) s;
            builder.mustNot(m -> m.prefix(pb -> pb.field(search.getField()).value(search.getQuery())));
            return;
        }
        if(s instanceof PhraseSearch) {
            PhraseSearch search = (PhraseSearch) s;
            builder.mustNot(m -> m.matchPhrase(pb -> {
                pb.field(search.getField()).query(search.getQuery());
                return search.useAnalyzer() ? pb.analyzer(search.getAnalyzer()) : pb;
            }));
            return;
        }
        if(s instanceof RangeSearch) {
            RangeSearch search = (RangeSearch) s;
            builder.mustNot(m -> m.range(search::putRange));
            return;
        }
        if(s instanceof TermsSearch) {
            TermsSearch search = (TermsSearch) s;
            builder.mustNot(m -> m.terms(tsb -> tsb.field(search.getField()).terms(t -> t.value(search.getValues()))));
        }
    }
}
